﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.IO;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;

namespace Templay
{
    /// <summary>
    /// Template object for parsing and play a template
    /// </summary>
    public class Template : IDisposable
    {
        
        private RootElement root;
        private Assembly assembly;

        private string currentAssemblyName;
        private Assembly currentAssembly;

        private List<Tag> tags;

        public string Content { get; set; }

        internal IParser parser;

        //Events
        public delegate void CompletedEventHandler();

        /// <summary>
        /// Raise when the render is completed
        /// </summary>
        public event CompletedEventHandler CompletedRender;

        /// <summary>
        /// Raise when the template have been runned
        /// </summary>
        public event CompletedEventHandler CompletedRun;

        /// <summary>
        /// All external pointers to referenced objects
        /// </summary>
        internal Dictionary<string, int> externalReferences;

        /// <summary>
        /// Construct default template object with xml parser
        /// </summary>
        public Template() : this(new XmlParser()) { }

        /// <summary>
        /// Construct default template object with xml parser
        /// </summary>
        /// <param name="parser">The parser you need to use</param>
        public Template(IParser parser)
        {
            this.parser = parser;
            currentAssembly = Assembly.GetExecutingAssembly();
            currentAssemblyName = currentAssembly.GetName().Name;

            externalReferences = new Dictionary<string, int>();
        }

        /// <summary>
        /// Construct template with loading of plugin parser (dll)
        /// </summary>
        /// <param name="parserPath">The dll which contain the parser</param>
        public Template(string parserPath)
        {
            try
            {
                Assembly parserAssembly = Assembly.LoadFile(parserPath);
                IParser parser = parserAssembly.CreateInstance("Parser") as IParser;

                if (parser == null)
                    throw new Exception("The parser is not correct : The Parser class must implements IParser from Templay");

                this.parser = parser;
            }
            catch (Exception ex)
            {
                throw new Exception("The parser is not correct : it doesn't contain Parser class", ex);
            }

            currentAssembly = Assembly.GetExecutingAssembly();
            currentAssemblyName = currentAssembly.GetName().Name;

            externalReferences = new Dictionary<string, int>();
        }

        /// <summary>
        /// Add object in references
        /// </summary>
        /// <param name="name">Name of reference</param>
        /// <param name="model">Object to add</param>
        public void AddExternalReference(string name, object model)
        {
            //Allocate reference of the model by GC
            GCHandle handle = GCHandle.Alloc(model);
            IntPtr pointer = GCHandle.ToIntPtr(handle);
            externalReferences.Add(name, pointer.ToInt32());
        }

        /// <summary>
        /// Remove object in references
        /// </summary>
        /// <param name="name">Name of object reference</param>
        public void RemoveReference(string name)
        {
            if (externalReferences.ContainsKey(name))
            {
                GCHandle handle = GCHandle.FromIntPtr(new IntPtr(externalReferences[name]));
                handle.Free();
                externalReferences.Remove(name);
            }
        }

        /// <summary>
        /// Include regex
        /// </summary>
        private Regex regex = new Regex("#include:(?<Url>.*?);", RegexOptions.Compiled);

        private void GetAllTemplate(string path, ref StringBuilder allTemplate, Func<string, string> templateEngineAction)
        {
            Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);

            using (StreamReader reader = new StreamReader(stream))
            {
                string strLine;
                while ((strLine = reader.ReadLine()) != null)
                    GetLineTemplate(strLine, ref allTemplate, templateEngineAction);

            }
        }

        private void GetLineTemplate(string strLine, ref StringBuilder allTemplate, Func<string, string> templateEngineAction)
        {
            if (templateEngineAction != null && Content != String.Empty)
                strLine = templateEngineAction(strLine).Replace("#data:", "");

            MatchCollection include = regex.Matches(strLine);

            foreach (Match url in include)
            {
                string strUrl = url.Groups["Url"].Value;
                strLine = strLine.Replace(url.Groups[0].Value, String.Empty);
                GetAllTemplate(url.Groups["Url"].Value, ref allTemplate, templateEngineAction);
            }

            allTemplate.Append(strLine);
        }

        /// <summary>
        /// Read and render the template
        /// </summary>
        /// <param name="content">The template path or template string</param>
        /// <param name="mode">The open mode of template</param>
        public void Render(string content, OpenMode mode)
        {
            Render(content, mode, null);
        }
        
        /// <summary>
        /// Read and render the template
        /// </summary>
        /// <param name="content">The template path or template string</param>
        /// <param name="mode">The open mode of template</param>
        /// <param name="templateEngineAction">If you want parse with template engine like Razor/DotLiquid...</param>
        public void Render(string content, OpenMode mode, Func<string, string> templateEngineAction)
        {
            StringBuilder allTemplate = new StringBuilder();

            if (mode == OpenMode.FILE)
                GetAllTemplate(content, ref allTemplate, templateEngineAction);
            else
                GetLineTemplate(content, ref allTemplate, templateEngineAction);

            Content = allTemplate.ToString();

            ////Template engine work's
            //if (templateEngineAction != null && Content != String.Empty)
            //    Content = templateEngineAction(Content).Replace("#data:", "");

            ParseElementCollection elements = parser.Parse(Content);
            root = elements.Root;
            elements.ApplyStyle();

            assembly = Assembly.Load(root.Assembly);

            tags = new List<Tag>();
            CreateTags(elements, ref tags);

            //Raise event for render
            if (CompletedRender != null)
                CompletedRender();
        }

        /// <summary>
        /// Recursive for create tag/Tags elements
        /// </summary>
        /// <param name="elements">Parsed elements</param>
        /// <param name="outTags">The out tag list created by elements</param>
        /// <param name="parent">Parent of tag if it exist</param>
        private void CreateTags(IEnumerable<ParseElement> elements, ref List<Tag> outTags, Tag parent = null)
        {
            //Create all Tag
            foreach (ParseElement element in elements)
            {
                string instance;

                Tag tag;

                if (element.Type == ParseElementType.ROOT)
                {
                    instance = String.Format("{0}.DefaultTags.{1}", currentAssemblyName, element.Name);
                    tag = currentAssembly.CreateInstance(instance) as Tag;
                }
                else
                {
                    instance = String.Format("{0}.{1}", root.Namespace, element.Name);
                    tag = assembly.CreateInstance(instance) as Tag;
                }

                //If the tag is null take the next parsed element
                if (tag == null)
                    continue;

                //Initialize
                tag.Initialize();

                //Create childrens
                List<Tag> outChildrens = new List<Tag>();
                CreateTags(element.Elements, ref outChildrens, tag);
                tag.Childrens = outChildrens;

                //Set parent
                if (parent != null)
                    tag.Parent = parent;

                //Set all properties
                foreach (ParseAttribute attribute in element.Attributes.Values)
                    tag.SetProperty(attribute.Name, attribute.Value, externalReferences);

                //Check required properties
                tag.FetchRequiredProperties();

                //Run init process after setting properties
                tag.InitProcess();

                outTags.Add(tag); // Accumulate for return (by ref)

            }
        }

        /// <summary>
        /// Process all parents tags/Tags.
        /// The process can be catch by overriding the Process(params object[] parameters) method
        /// on each tag/Tag.
        /// </summary>
        /// <param name="parameters">Any parameters you need to send</param>
        public void Run(params object[] parameters)
        {
            foreach (Tag tag in tags)
                tag.Process();

            //Raise event for run
            if (CompletedRun != null)
                CompletedRun();
        }

        /// <summary>
        /// Dispose the template
        /// </summary>
        public void Dispose()
        {
            //Free memory
            foreach (IntPtr pointer in externalReferences.Values)
                GCHandle.FromIntPtr(pointer).Free();
        }

    }

    /// <summary>
    /// Template object for parsing and play a template
    /// </summary>
    /// <typeparam name="TParser">The parser you need to use, it must implements IParser</typeparam>
    public class Template<TParser> : Template 
        where TParser : IParser
    {

        /// <summary>
        /// Construct default template object with xml parser
        /// </summary>
        public Template()
        {
            TParser parser = Activator.CreateInstance<TParser>();
            base.parser = parser;
        }

    }

    public enum OpenMode
    {
        STRING,
        FILE
    }

}
